Skip to content

docs(workflows): send Nodemailer mail through PostHog - #21987

Draft
Silthus wants to merge 29 commits into
masterfrom
docs/transactional-email-nodemailer
Draft

Silthus wants to merge 29 commits into
masterfrom
docs/transactional-email-nodemailer

Conversation

@Silthus

@Silthus Silthus commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Changes

A developer whose app sends email with Nodemailer finds no page that tells them how to send that email through PostHog.

This PR adds Send transactional emails with Nodemailer under Send transactional emails. The page shows how to install posthog-node and nodemailer, create the transport with createPostHogTransport from posthog-node/nodemailer, and keep each sendMail call. It also covers the key and host, the posthog options for one message, a stable idempotency_key for mail from a job queue, and the messages that PostHog refuses (EPOSTHOGUNSUPPORTED). The Node.js page's code-send section links to the page, and the sidebar gets a Nodemailer entry after Node.js.

Ticket: https://github.com/PostHog/michaelr-context/issues/41 (private).

Stacked on #21819. Merge it first. The transport ships in posthog-node with the PostHog/posthog-js draft PRs of the same ticket. This page stays a draft until that release is out.

Seams for review. The sidebar in src/navs/index.js is a hot file. #21932 adds SDK entries between Python and API, and #21984 adds React Email after API. This PR adds its entry after Node.js, so its hunk touches neither. Check for cross-stack breaks: the same concept documented twice (retries and idempotency keys on the Node.js page and on this page), and anything that conflicts with #21932 or #21984.

Merge danger

Low, two-way door. Docs only: one new page, one link, and one sidebar entry. A revert removes them.

Evidence

  • prettier --check src/navs/index.js passes.
  • Page check and before/after sidebar screenshots: these follow from the preview build.

Decisions

Question Answer Who Where
Where does the sidebar entry go? After Node.js, so its hunk does not touch #21932 or #21984 👍 Accepted default ticket
Does the page document every refusal? Yes, one table row for each refusal code, with the fix where one exists 🤖 Agent ticket

Checklist

  • I've read the docs and/or content style guides.
  • Words are spelled using American English
  • Use relative URLs for internal links
  • I've checked the pages added or changed in the Vercel preview build
  • If I moved a page, I added a redirect in vercel.json (no page moved)

🤖 Generated with Claude Code

mayteio and others added 28 commits October 24, 2026 10:38
Document how to make a Workflows email template transactional and send it with POST /api/projects/:project_id/messaging_templates/:template_id/send/. The page covers the API key, the request and response, idempotency keys, error codes, rate limits, and how transactional emails behave. The page is marked beta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: a0274fa0-d9be-48bf-b601-f153decf241d
…e SDKs

- Send through `POST /messaging/send/email/` with `template.id`. The template route still works.
- Read an email's status with `GET /messaging/emails/:message_id/`: statuses, reasons, 404 cases, scopes and read limits.
- Send and read with the Python and Node.js SDKs.
- Add `recipient_unreachable` to the errors table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
- The Node.js example turns off local evaluation, and both examples keep the SDK's error.
- The status examples handle the error first and explain retrying a not_found.
- The SDK section adds install, client reuse, timeouts and retries, uncertain outcomes, and a no-network testing example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…m pages

The overview keeps the template, key, idempotency, errors, statuses and behavior. Node.js, Python and API pages hold the setup, send, status, retries and testing for each platform, with a sidebar group like push notifications.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…he overview

- A quick start links to the Node.js, Python and API pages.
- Creating an email follows the Transactional section's wizard, and the hidden workflow explanation is gone.
- Agents can set it up with the PostHog MCP server.
- Protect sensitive data turns off click tracking and keeps deliveries, opens and statuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
The send endpoint now takes a flat body (PostHog/posthog#126287), so the field table, the curl example and the engagement events note use template_id and distinct_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…il API page

Document the code form of the send route (template_key with the subject
and body), capture_engagement, the checks that only apply to it, and a
delivery_unavailable anchor for the 503's docs link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mits

Add DKIM-Signature and Received to the reserved headers, the header name
and value limits, the tag length, and that reply_to takes bare addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t template_key and ph_ tags do

- A retry with the same Idempotency-Key is the same request when every field of the code-form table matches.
- template_key groups the email's metrics and sends.
- A tag name that starts with ph_, in any case, is refused.
- capture_engagement on a template send defaults to the project's setting.
- Drop a double blank line after the checks table (fix-mdx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ell out the ph_ case rule

- The template-send table gets a capture_engagement row with the project default, and says a retry sends the field the same way.
- The code-form row keeps only its own default.
- A ph_ tag name is refused in uppercase or lowercase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	contents/docs/workflows/transactional-emails/api.mdx
#	contents/docs/workflows/transactional-emails/node.mdx
#	contents/docs/workflows/transactional-emails/python.mdx
…code send

The first release sends content from code with template_key, subject, html and text only. React Email output is rendered by the app and sent as html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop capture_engagement from the template send: the send route
  refuses it there, with the other fields of content from your code.
- Fold sender_not_allowed into sender_not_verified, the code the
  first release returns for a from address no verified sender can use.
- Add "Send content from your code" to the Node.js and Python pages,
  with a React Email example rendered by the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in a replay

- A field error is a 400 with type validation_error, a field code
  such as required or invalid, and attr naming the field.
- A replay also compares the sender PostHog chose, so a retry without
  from after the default sender changed returns idempotency_key_reused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntify distinct ID

Add "Link emails to your users" to the API, Node.js and Python pages: pass
the distinct ID you pass to identify(), what happens without one today (a
separate person keyed by the email address), and that invites and signup
checks can leave it out. The overview and the API tables point there
instead of "always send it".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Node.js and Python client examples used <ph_project_api_key> and a
hard-coded ingestion host, which the site doesn't replace. They now use
<ph_project_token> and <ph_client_api_host>, as the SDK pages do, so a
logged-in reader sees their own project token and host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atus reads

A send needs transactional_email:write and a status read needs
transactional_email:read or :write. Keys with a hog_flow scope still work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The key steps still picked Workflow, which creates a hog_flow key. The
scopes list names the new scope Transactional email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… email scope

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A status read right after a send can answer 404 for a few seconds,
until the send's first status arrives. Say so where the API page
lists the 404, and that a retry after a short wait is the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
posthog-python#1171 ships as a minor bump, and PyPI is already at
7.83.2, so the code send arrives in 7.84.0, not 7.83.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a Nodemailer page under Send transactional emails: install, the
PostHog transport, key and host, the posthog options, retries from a
queue with a stable idempotency_key, and the messages PostHog refuses.
Link it from the Node.js page and add a sidebar entry after Node.js.

Ticket: PostHog/michaelr-context#41

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Deploy preview

Status Details Updated (UTC)
🟢 Ready View preview Nov 05, 2026 01:15PM

Changed pages

Page Source
Send transactional emails with Node.js contents/docs/workflows/transactional-emails/node.mdx
Send transactional emails with Nodemailer contents/docs/workflows/transactional-emails/nodemailer.mdx

@github-actions github-actions Bot added the docs Improvements or additions to product documentation, "Docs" label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Vale prose linter → found 0 errors, 5 warnings, 0 suggestions in your markdown

Full report → Copy the linter results into an LLM to batch-fix issues.

Linter being weird? Update the rules!

contents/docs/workflows/transactional-emails/node.mdx — 0 errors, 1 warnings, 0 suggestions
Line Severity Message Rule
102:160 warning 'Nodemailer' is a possible misspelling. PostHogBase.Spelling
contents/docs/workflows/transactional-emails/nodemailer.mdx — 0 errors, 4 warnings, 0 suggestions
Line Severity Message Rule
2:39 warning 'Nodemailer' is a possible misspelling. PostHogBase.Spelling
7:31 warning 'Nodemailer' is a possible misspelling. PostHogBase.Spelling
19:96 warning 'Nodemailer' is a possible misspelling. PostHogBase.Spelling
82:70 warning 'Nodemailer's' is a possible misspelling. PostHogBase.Spelling

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 7c0f31f: 1 Should fix.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.


- **`template_key`** names the email, such as `order-shipped`. PostHog groups the email's metrics and sends under it. Without it, the transport uses `defaultTemplateKey` from `createPostHogTransport`, or `nodemailer`. Use letters, digits, hyphens, and underscores.
- **`distinct_id`** records the email against the person with this distinct ID. Without it, PostHog uses a person whose distinct ID is the `to` address. See [link emails to your users](/docs/workflows/transactional-emails/node#link-emails-to-your-users).
- **`idempotency_key`** identifies this one email. PostHog sends an email only once for each key.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · Limit the idempotency guarantee to its 24-hour window

When a queue retries a send more than 24 hours after PostHog queued it, the same idempotency_key no longer prevents another email. The transactional-email overview documents this expiration, but this unconditional guarantee tells readers that a stable key always prevents duplicates. State the 24-hour window and link the queue guidance to that limit.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Bundle report

Total JS (gzip)

10.27 MiB (-1480.1 KiB / -12.3%)

Largest changed named chunks
Chunk Gzip Δ vs master
app.js 2361.2 KiB +1270.1 KiB / +116.4%
23b110d95c587435d17e5a559280a7b225e5608c.js 110.1 KiB new
655c369601e38a7136cffdc999798d51fd71fc19.js 50.7 KiB new
4e5c9a1d4810c5f5c6325835bcfd7f436eb1af7e.js 9.3 KiB -33.4 KiB / -78.2%
d874fb577ecb130d9c247b410613f0930a236c94.js 20.0 KiB new
af98e9f54e85b4e4080569f09ec6b2f06c3d7b85.js 15.0 KiB new
component---src-pages-careers-tsx.js 54.5 KiB -10.9 KiB / -16.6%
component---src-pages-r-product-analytics-tsx.js 6.7 KiB -9.4 KiB / -58.3%
component---src-pages-r-posthog-mcp-tsx.js 4.3 KiB -9.3 KiB / -68.2%
component---src-pages-merch-tsx.js 22.0 KiB -8.8 KiB / -28.5%
component---src-pages-docs-about-tsx.js 0.8 KiB -7.4 KiB / -90.5%
component---src-pages-self-driving-index-tsx.js 9.2 KiB -7.3 KiB / -44.1%
component---src-pages-changelog-index-tsx.js 0.1 KiB -6.1 KiB / -97.8%
component---src-pages-teams-[slug]-tsx.js 8.9 KiB -6.1 KiB / -40.5%
component---src-pages-hog-index-tsx.js 0.5 KiB -5.6 KiB / -91.9%

Eager graph (modules shipped in each entrypoint's initial chunks)

Entrypoint Eager size Budget Modules
✅ app 20.92 MiB (+11246.9 KiB / +110.5%) report-only 2119
Largest modules in the app closure
Module Size
./src/data/mcp-tools.json 1491.9 KiB
css ./node_modules/.pnpm/css-loader@5.2.7_webpack@5.101.3/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[1]!./node_modules/.pnpm/postcss-loader@4.3.0_postcss@8.5.6_webpack@5.101.3/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[8].oneOf[1].use[2]!./src/styles/global.css 838.0 KiB
./src/components/Stickers/Stickers.tsx 696.4 KiB
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/x-ray.mjs 481.2 KiB
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/trenchcoat.mjs 450.8 KiB
./node_modules/.pnpm/rehype-raw@7.0.0/node_modules/rehype-raw/lib/index.js + 29 modules 395.1 KiB
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/im-the-driver.mjs 385.8 KiB
./src/hooks/useCustomers.tsx + 61 modules 381.8 KiB
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/tourist.mjs 364.6 KiB
./node_modules/.pnpm/@posthog+icons@0.36.6_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js 354.8 KiB
./node_modules/.pnpm/react-markdown@8.0.7_@types+react@16.14.66_react@18.3.1/node_modules/react-markdown/lib/react-markdown.js + 88 modules 351.4 KiB
./src/components/ProductComparisonTable/index.tsx + 130 modules 314.3 KiB
./node_modules/.pnpm/framer-motion@14.0.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/framer-motion/dist/es/render/components/motion/proxy.mjs + 126 modules 287.2 KiB
./node_modules/.pnpm/@posthog+brand@0.13.0_react@18.3.1/node_modules/@posthog/brand/dist/generated/hoggies/svg/doll-house.mjs 284.1 KiB
./node_modules/.pnpm/cloudinary-core@2.14.0_lodash@4.17.21/node_modules/cloudinary-core/cloudinary-core.js 281.9 KiB

Eager-graph budgets are report-only until a baseline is established. Sizes are gzip of public/**/*.js; eager size is webpack module source bytes for the modules actually shipped in the entrypoint's initial chunks (post-tree-shake).

Name the 24-hour key window for queued mail, and list the attachment refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from docs/transactional-email-api-code-form to master November 6, 2026 07:40

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to product documentation, "Docs"

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants